/*!
 * qiniu-js-sdk v@VERSION
 *
 * Copyright 2015 by Qiniu
 * Released under GPL V2 License.
 *
 * GitHub: http://github.com/qiniu/js-sdk
 *
 * Date: @DATE
 */

;(function (global){

  /**
   * Creates new cookie or removes cookie with negative expiration
   * @param  key       The key or identifier for the store
   * @param  value     Contents of the store
   * @param  exp       Expiration - creation defaults to 30 days
   */
  function createCookie(key, value, exp) {
    var date = new Date();
    date.setTime(date.getTime() + (exp * 24 * 60 * 60 * 1000));
    var expires = "; expires=" + date.toGMTString();
    document.cookie = key + "=" + value + expires + "; path=/";
  }

  /**
   * Returns contents of cookie
   * @param  key       The key or identifier for the store
   */
  function readCookie(key) {
    var nameEQ = key + "=";
    var ca = document.cookie.split(';');
    for (var i = 0, max = ca.length; i < max; i++) {
      var c = ca[i];
      while (c.charAt(0) === ' ') {
        c = c.substring(1, c.length);
      }
      if (c.indexOf(nameEQ) === 0) {
        return c.substring(nameEQ.length, c.length);
      }
    }
    return null;
  }

// if current browser is not support localStorage
// use cookie to make a polyfill
  if ( !window.localStorage ) {
    window.localStorage = {
      setItem: function (key, value) {
        createCookie(key, value, 30);
      },
      getItem: function (key) {
        return readCookie(key);
      },
      removeItem: function (key) {
        createCookie(key, '', -1);
      }
    };
  }

  function QiniuJsSDK() {

    var that = this;

    /**
     * detect IE version
     * if current browser is not IE
     *     it will return false
     * else
     *     it will return version of current IE browser
     * @return {Number|Boolean} IE version or false
     */
    this.detectIEVersion = function() {
      var v = 4,
        div = document.createElement('div'),
        all = div.getElementsByTagName('i');
      while (
        div.innerHTML = '<!--[if gt IE ' + v + ']><i></i><![endif]-->',
          all[0]
        ) {
        v++;
      }
      return v > 4 ? v : false;
    };

    var logger = {
      MUTE: 0,
      FATA: 1,
      ERROR: 2,
      WARN: 3,
      INFO: 4,
      DEBUG: 5,
      TRACE: 6,
      level: 0
    };

    function log(type, args){
      var header = "[qiniu-js-sdk]["+type+"]";
      var msg = header;
      for (var i = 0; i < args.length; i++) {
        if (typeof args[i] === "string") {
          msg += " " + args[i];
        } else {
          msg += " " + that.stringifyJSON(args[i]);
        }
      }
      if (that.detectIEVersion()) {
        // http://stackoverflow.com/questions/5538972/console-log-apply-not-working-in-ie9
        //var log = Function.prototype.bind.call(console.log, console);
        //log.apply(console, args);
        console.log(msg);
      }else{
        args.unshift(header);
        console.log.apply(console, args);
      }
      if (document.getElementById('qiniu-js-sdk-log')) {
        document.getElementById('qiniu-js-sdk-log').innerHTML += '<p>'+msg+'</p>';
      }
    }

    function makeLogFunc(code){
      var func = code.toLowerCase();
      logger[func] = function(){
        // logger[func].history = logger[func].history || [];
        // logger[func].history.push(arguments);
        if(window.console && window.console.log && logger.level>=logger[code]){
          var args = Array.prototype.slice.call(arguments);
          log(func,args);
        }
      };
    }

    for (var property in logger){
      if (logger.hasOwnProperty(property) && (typeof logger[property]) === "number" && !logger.hasOwnProperty(property.toLowerCase())) {
        makeLogFunc(property);
      }
    }


    var qiniuUploadUrl;
    if (window.location.protocol === 'https:') {
      qiniuUploadUrl = 'https://up.qbox.me';
    } else {
      qiniuUploadUrl = 'http://upload.qiniu.com';
    }

    /**
     * qiniu upload urls
     * 'qiniuUploadUrls' is used to change target when current url is not avaliable
     * @type {Array}
     */
    var qiniuUploadUrls = [
      "http://upload.qiniu.com",
      "http://up.qiniu.com",
    ];

    var changeUrlTimes = 0;

    /**
     * reset upload url
     * if current page protocal is https
     *     it will always return 'https://up.qbox.me'
     * else
     *     it will set 'qiniuUploadUrl' value with 'qiniuUploadUrls' looply
     */
    this.resetUploadUrl = function(){
      if (window.location.protocol === 'https:') {
        qiniuUploadUrl = 'https://up.qbox.me';
      } else {
        var i = changeUrlTimes % qiniuUploadUrls.length;
        qiniuUploadUrl = qiniuUploadUrls[i];
        changeUrlTimes++;
      }
      logger.debug('resetUploadUrl: '+qiniuUploadUrl);
    };

    this.resetUploadUrl();


    /**
     * is image
     * @param  {String}  url of a file
     * @return {Boolean} file is a image or not
     */
    this.isImage = function(url) {
      url = url.split(/[?#]/)[0];
      return (/\.(png|jpg|jpeg|gif|bmp)$/i).test(url);
    };

    /**
     * get file extension
     * @param  {String} filename
     * @return {String} file extension
     * @example
     *     input: test.txt
     *     output: txt
     */
    this.getFileExtension = function(filename) {
      var tempArr = filename.split(".");
      var ext;
      if (tempArr.length === 1 || (tempArr[0] === "" && tempArr.length === 2)) {
        ext = "";
      } else {
        ext = tempArr.pop().toLowerCase(); //get the extension and make it lower-case
      }
      return ext;
    };

    /**
     * encode string by utf8
     * @param  {String} string to encode
     * @return {String} encoded string
     */
    this.utf8_encode = function(argString) {
      // http://kevin.vanzonneveld.net
      // +   original by: Webtoolkit.info (http://www.webtoolkit.info/)
      // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
      // +   improved by: sowberry
      // +    tweaked by: Jack
      // +   bugfixed by: Onno Marsman
      // +   improved by: Yves Sucaet
      // +   bugfixed by: Onno Marsman
      // +   bugfixed by: Ulrich
      // +   bugfixed by: Rafal Kukawski
      // +   improved by: kirilloid
      // +   bugfixed by: kirilloid
      // *     example 1: this.utf8_encode('Kevin van Zonneveld');
      // *     returns 1: 'Kevin van Zonneveld'

      if (argString === null || typeof argString === 'undefined') {
        return '';
      }

      var string = (argString + ''); // .replace(/\r\n/g, '\n').replace(/\r/g, '\n');
      var utftext = '',
        start, end, stringl = 0;

      start = end = 0;
      stringl = string.length;
      for (var n = 0; n < stringl; n++) {
        var c1 = string.charCodeAt(n);
        var enc = null;

        if (c1 < 128) {
          end++;
        } else if (c1 > 127 && c1 < 2048) {
          enc = String.fromCharCode(
            (c1 >> 6) | 192, (c1 & 63) | 128
          );
        } else if (c1 & 0xF800 ^ 0xD800 > 0) {
          enc = String.fromCharCode(
            (c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128
          );
        } else { // surrogate pairs
          if (c1 & 0xFC00 ^ 0xD800 > 0) {
            throw new RangeError('Unmatched trail surrogate at ' + n);
          }
          var c2 = string.charCodeAt(++n);
          if (c2 & 0xFC00 ^ 0xDC00 > 0) {
            throw new RangeError('Unmatched lead surrogate at ' + (n - 1));
          }
          c1 = ((c1 & 0x3FF) << 10) + (c2 & 0x3FF) + 0x10000;
          enc = String.fromCharCode(
            (c1 >> 18) | 240, ((c1 >> 12) & 63) | 128, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128
          );
        }
        if (enc !== null) {
          if (end > start) {
            utftext += string.slice(start, end);
          }
          utftext += enc;
          start = end = n + 1;
        }
      }

      if (end > start) {
        utftext += string.slice(start, stringl);
      }

      return utftext;
    };

    /**
     * encode data by base64
     * @param  {String} data to encode
     * @return {String} encoded data
     */
    this.base64_encode = function(data) {
      // http://kevin.vanzonneveld.net
      // +   original by: Tyler Akins (http://rumkin.com)
      // +   improved by: Bayron Guevara
      // +   improved by: Thunder.m
      // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
      // +   bugfixed by: Pellentesque Malesuada
      // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
      // -    depends on: this.utf8_encode
      // *     example 1: this.base64_encode('Kevin van Zonneveld');
      // *     returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA=='
      // mozilla has this native
      // - but breaks in 2.0.0.12!
      //if (typeof this.window['atob'] == 'function') {
      //    return atob(data);
      //}
      var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
      var o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
        ac = 0,
        enc = '',
        tmp_arr = [];

      if (!data) {
        return data;
      }

      data = this.utf8_encode(data + '');

      do { // pack three octets into four hexets
        o1 = data.charCodeAt(i++);
        o2 = data.charCodeAt(i++);
        o3 = data.charCodeAt(i++);

        bits = o1 << 16 | o2 << 8 | o3;

        h1 = bits >> 18 & 0x3f;
        h2 = bits >> 12 & 0x3f;
        h3 = bits >> 6 & 0x3f;
        h4 = bits & 0x3f;

        // use hexets to index into b64, and append result to encoded string
        tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
      } while (i < data.length);

      enc = tmp_arr.join('');

      switch (data.length % 3) {
        case 1:
          enc = enc.slice(0, -2) + '==';
          break;
        case 2:
          enc = enc.slice(0, -1) + '=';
          break;
      }

      return enc;
    };

    /**
     * encode string in url by base64
     * @param {String} string in url
     * @return {String} encoded string
     */
    this.URLSafeBase64Encode = function(v) {
      v = this.base64_encode(v);
      return v.replace(/\//g, '_').replace(/\+/g, '-');
    };

    // TODO: use mOxie
    /**
     * craete object used to AJAX
     * @return {Object}
     */
    this.createAjax = function(argument) {
      var xmlhttp = {};
      if (window.XMLHttpRequest) {
        xmlhttp = new XMLHttpRequest();
      } else {
        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
      }
      return xmlhttp;
    };

    // TODO: enhance IE compatibility
    /**
     * parse json string to javascript object
     * @param  {String} json string
     * @return {Object} object
     */
    this.parseJSON = function(data) {
      // Attempt to parse using the native JSON parser first
      if (window.JSON && window.JSON.parse) {
        return window.JSON.parse(data);
      }

      //var rx_one = /^[\],:{}\s]*$/,
      //    rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,
      //    rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,
      //    rx_four = /(?:^|:|,)(?:\s*\[)+/g,
      var    rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;

      //var json;

      var text = String(data);
      rx_dangerous.lastIndex = 0;
      if(rx_dangerous.test(text)){
        text = text.replace(rx_dangerous, function(a){
          return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
        });
      }

      // todo 使用一下判断,增加安全性
      //if (
      //    rx_one.test(
      //        text
      //            .replace(rx_two, '@')
      //            .replace(rx_three, ']')
      //            .replace(rx_four, '')
      //    )
      //) {
      //    return eval('(' + text + ')');
      //}

      return eval('('+text+')');
    };

    /**
     * parse javascript object to json string
     * @param  {Object} object
     * @return {String} json string
     */
    this.stringifyJSON = function(obj) {
      // Attempt to parse using the native JSON parser first
      if (window.JSON && window.JSON.stringify) {
        return window.JSON.stringify(obj);
      }
      switch (typeof (obj)) {
        case 'string':
          return '"' + obj.replace(/(["\\])/g, '\\$1') + '"';
        case 'array':
          return '[' + obj.map(that.stringifyJSON).join(',') + ']';
        case 'object':
          if (obj instanceof Array) {
            var strArr = [];
            var len = obj.length;
            for (var i = 0; i < len; i++) {
              strArr.push(that.stringifyJSON(obj[i]));
            }
            return '[' + strArr.join(',') + ']';
          } else if (obj === null) {
            return 'null';
          } else {
            var string = [];
            for (var property in obj) {
              if (obj.hasOwnProperty(property)) {
                string.push(that.stringifyJSON(property) + ':' + that.stringifyJSON(obj[property]));
              }
            }
            return '{' + string.join(',') + '}';
          }
          break;
        case 'number':
          return obj;
        case false:
          return obj;
        case 'boolean':
          return obj;
      }
    };

    /**
     * trim space beside text
     * @param  {String} untrimed string
     * @return {String} trimed string
     */
    this.trim = function(text) {
      return text === null ? "" : text.replace(/^\s+|\s+$/g, '');
    };

    /**
     * create a uploader by QiniuJsSDK
     * @param  {object} options to create a new uploader
     * @return {object} uploader
     */
    this.uploader = function(op) {

      /********** inner function define start **********/

        // according the different condition to reset chunk size
        // and the upload strategy according with the chunk size
        // when chunk size is zero will cause to direct upload
        // see the statement binded on 'BeforeUpload' event
      var reset_chunk_size = function() {
          var ie = that.detectIEVersion();
          var BLOCK_BITS, MAX_CHUNK_SIZE, chunk_size;
          // case Safari 5、Windows 7、iOS 7 set isSpecialSafari to true
          var isSpecialSafari = (mOxie.Env.browser === "Safari" && mOxie.Env.version <= 5 && mOxie.Env.os === "Windows" && mOxie.Env.osVersion === "7") || (mOxie.Env.browser === "Safari" && mOxie.Env.os === "iOS" && mOxie.Env.osVersion === "7");
          // case IE 9-，chunk_size is not empty and flash is included in runtimes
          // set op.chunk_size to zero
          //if (ie && ie <= 9 && op.chunk_size && op.runtimes.indexOf('flash') >= 0) {
          if (ie && ie <= 9 && op.chunk_size && op.runtimes.indexOf('flash') < 0) {
            //  link: http://www.plupload.com/docs/Frequently-Asked-Questions#when-to-use-chunking-and-when-not
            //  when plupload chunk_size setting is't null ,it cause bug in ie8/9  which runs  flash runtimes (not support html5) .
            op.chunk_size = 0;
          } else if (isSpecialSafari) {
            // win7 safari / iOS7 safari have bug when in chunk upload mode
            // reset chunk_size to 0
            // disable chunk in special version safari
            op.chunk_size = 0;
          } else {
            BLOCK_BITS = 20;
            MAX_CHUNK_SIZE = 4 << BLOCK_BITS; //4M

            chunk_size = plupload.parseSize(op.chunk_size);
            if (chunk_size > MAX_CHUNK_SIZE) {
              op.chunk_size = MAX_CHUNK_SIZE;
            }
            // qiniu service  max_chunk_size is 4m
            // reset chunk_size to max_chunk_size(4m) when chunk_size > 4m
          }
          // if op.chunk_size set 0 will be cause to direct upload
        };

      // getUptoken maybe called at Init Event or BeforeUpload Event
      // case Init Event, the file param of getUptken will be set a null value
      // if op.uptoken has value, set uptoken with op.uptoken
      // else if op.uptoken_url has value, set uptoken from op.uptoken_url
      // else if op.uptoken_func has value, set uptoken by result of op.uptoken_func
      var getUpToken = function(file) {
        if (op.uptoken) {
          that.token = op.uptoken;
          return;
        } else if (op.uptoken_url) {
          logger.debug("get uptoken from: ", that.uptoken_url);
          // TODO: use mOxie
          var ajax = that.createAjax();
          ajax.open('GET', that.uptoken_url, false);
          ajax.setRequestHeader("If-Modified-Since", "0");
          // ajax.onreadystatechange = function() {
          //     if (ajax.readyState === 4 && ajax.status === 200) {
          //         var res = that.parseJSON(ajax.responseText);
          //         that.token = res.uptoken;
          //     }
          // };
          ajax.send();
          if (ajax.status === 200) {
            var res = that.parseJSON(ajax.responseText);
            that.token = res.uptoken;
            logger.debug("get new uptoken: ", res.uptoken);
          } else {
            logger.error("get uptoken error: ", ajax.responseText);
          }
          return;
        } else if (op.uptoken_func) {
          logger.debug("get uptoken from uptoken_func");
          that.token = op.uptoken_func(file);
          logger.debug("get new uptoken: ", that.token);
          return;
        } else {
          logger.error("one of [uptoken, uptoken_url, uptoken_func] settings in options is required!");
        }
      };

      // get file key according with the user passed options
      var getFileKey = function(up, file, func) {
        // TODO: save_key can read from scope of token
        var key = '',
          unique_names = false;
        if (!op.save_key) {
          unique_names = up.getOption && up.getOption('unique_names');
          unique_names = unique_names || (up.settings && up.settings.unique_names);
          if (unique_names) {
            var ext = that.getFileExtension(file.name);
            key = ext ? file.id + '.' + ext : file.id;
          } else if (typeof func === 'function') {
            key = func(up, file);
          } else {
            key = file.name;
          }
        }
        return key;
      };

      /********** inner function define end **********/

      if (op.log_level) {
        logger.level = op.log_level;
      }

      if (!op.domain) {
        throw 'domain setting in options is required!';
      }

      if (!op.browse_button) {
        throw 'browse_button setting in options is required!';
      }

      if (!op.uptoken && !op.uptoken_url && !op.uptoken_func) {
        throw 'one of [uptoken, uptoken_url, uptoken_func] settings in options is required!';
      }

      logger.debug("init uploader start");

      logger.debug("environment: ", mOxie.Env);

      logger.debug("userAgent: ", navigator.userAgent);

      var option = {};

      // hold the handler from user passed options
      var _Error_Handler = op.init && op.init.Error;
      var _FileUploaded_Handler = op.init && op.init.FileUploaded;

      // replace the handler for intercept
      op.init.Error = function() {};
      op.init.FileUploaded = function() {};

      that.uptoken_url = op.uptoken_url;
      that.token = '';
      that.key_handler = typeof op.init.Key === 'function' ? op.init.Key : '';
      this.domain = op.domain;
      // TODO: ctx is global in scope of a uploader instance
      // this maybe cause error
      var ctx = '';
      var speedCalInfo = {
        isResumeUpload: false,
        resumeFilesize: 0,
        startTime: '',
        currentTime: ''
      };

      reset_chunk_size();
      logger.debug("invoke reset_chunk_size()");
      logger.debug("op.chunk_size: ", op.chunk_size);

      // compose options with user passed options and default setting
      plupload.extend(option, op, {
        url: qiniuUploadUrl,
        multipart_params: {
          token: ''
        }
      });

      logger.debug("option: ", option);

      // create a new uploader with composed options
      var uploader = new plupload.Uploader(option);

      logger.debug("new plupload.Uploader(option)");

      // bind getUpToken to 'Init' event
      uploader.bind('Init', function(up, params) {
        logger.debug("Init event activated");
        // if op.get_new_uptoken is not true
        //      invoke getUptoken when uploader init
        // else
        //      getUptoken everytime before a new file upload
        if(!op.get_new_uptoken){
          getUpToken(null);
        }
        //getUpToken(null);
      });

      logger.debug("bind Init event");

      // bind 'FilesAdded' event
      // when file be added and auto_start has set value
      // uploader will auto start upload the file
      uploader.bind('FilesAdded', function(up, files) {
        logger.debug("FilesAdded event activated");
        var auto_start = up.getOption && up.getOption('auto_start');
        auto_start = auto_start || (up.settings && up.settings.auto_start);
        logger.debug("auto_start: ", auto_start);
        logger.debug("files: ", files);

        // detect is iOS
        var is_ios = function (){
          if(mOxie.Env.OS.toLowerCase()==="ios") {
            return true;
          } else {
            return false;
          }
        };

        // if current env os is iOS change file name to [time].[ext]
        if (is_ios()) {
          for (var i = 0; i < files.length; i++) {
            var file = files[i];
            var ext = that.getFileExtension(file.name);
            file.name = file.id + "." + ext;
          }
        }

        if (auto_start) {
          setTimeout(function(){
            up.start();
            logger.debug("invoke up.start()");
          }, 0);
          // up.start();
          // plupload.each(files, function(i, file) {
          //     up.start();
          //     logger.debug("invoke up.start()")
          //     logger.debug("file: ", file);
          // });
        }
        up.refresh(); // Reposition Flash/Silverlight
      });

      logger.debug("bind FilesAdded event");

      // bind 'BeforeUpload' event
      // intercept the process of upload
      // - prepare uptoken
      // - according the chunk size to make differnt upload strategy
      // - resume upload with the last breakpoint of file
      uploader.bind('BeforeUpload', function(up, file) {
        logger.debug("BeforeUpload event activated");
        // add a key named speed for file object
        file.speed = file.speed || 0;
        ctx = '';

        if(op.get_new_uptoken){
          getUpToken(file);
        }

        var directUpload = function(up, file, func) {
          speedCalInfo.startTime = new Date().getTime();
          var multipart_params_obj;
          if (op.save_key) {
            multipart_params_obj = {
              'token': that.token
            };
          } else {
            multipart_params_obj = {
              'key': getFileKey(up, file, func),
              'token': that.token
            };
          }

          logger.debug("directUpload multipart_params_obj: ", multipart_params_obj);

          var x_vars = op.x_vars;
          if (x_vars !== undefined && typeof x_vars === 'object') {
            for (var x_key in x_vars) {
              if (x_vars.hasOwnProperty(x_key)) {
                if (typeof x_vars[x_key] === 'function') {
                  multipart_params_obj['x:' + x_key] = x_vars[x_key](up, file);
                } else if (typeof x_vars[x_key] !== 'object') {
                  multipart_params_obj['x:' + x_key] = x_vars[x_key];
                }
              }
            }
          }


          up.setOption({
            'url': qiniuUploadUrl,
            'multipart': true,
            'chunk_size': is_android_weixin_or_qq() ? op.max_file_size : undefined,
            'multipart_params': multipart_params_obj
          });
        };

        // detect is weixin or qq inner browser
        var is_android_weixin_or_qq = function (){
          var ua = navigator.userAgent.toLowerCase();
          if((ua.match(/MicroMessenger/i) || mOxie.Env.browser === "QQBrowser" || ua.match(/V1_AND_SQ/i)) && mOxie.Env.OS.toLowerCase()==="android") {
            return true;
          } else {
            return false;
          }
        };

        var chunk_size = up.getOption && up.getOption('chunk_size');
        chunk_size = chunk_size || (up.settings && up.settings.chunk_size);

        logger.debug("uploader.runtime: ",uploader.runtime);
        logger.debug("chunk_size: ",chunk_size);

        // TODO: flash support chunk upload
        if ((uploader.runtime === 'html5' || uploader.runtime === 'flash') && chunk_size) {
          if (file.size < chunk_size || is_android_weixin_or_qq()) {
            logger.debug("directUpload because file.size < chunk_size || is_android_weixin_or_qq()");
            // direct upload if file size is less then the chunk size
            directUpload(up, file, that.key_handler);
          } else {
            // TODO: need a polifill to make it work in IE 9-
            // ISSUE: if file.name is existed in localStorage
            // but not the same file maybe cause error
            var localFileInfo = localStorage.getItem(file.name);
            var blockSize = chunk_size;
            if (localFileInfo) {
              // TODO: although only the html5 runtime will enter this statement
              // but need uniform way to make convertion between string and json
              localFileInfo = that.parseJSON(localFileInfo);
              var now = (new Date()).getTime();
              var before = localFileInfo.time || 0;
              var aDay = 24 * 60 * 60 * 1000; //  milliseconds of one day
              // if the last upload time is within one day
              //      will upload continuously follow the last breakpoint
              // else
              //      will reupload entire file
              if (now - before < aDay) {

                if (localFileInfo.percent !== 100) {
                  if (file.size === localFileInfo.total) {
                    // TODO: if file.name and file.size is the same
                    // but not the same file will cause error
                    file.percent = localFileInfo.percent;
                    file.loaded = localFileInfo.offset;
                    ctx = localFileInfo.ctx;

                    // set speed info
                    speedCalInfo.isResumeUpload = true;
                    speedCalInfo.resumeFilesize = localFileInfo.offset;

                    // set block size
                    if (localFileInfo.offset + blockSize > file.size) {
                      blockSize = file.size - localFileInfo.offset;
                    }
                  } else {
                    // remove file info when file.size is conflict with file info
                    localStorage.removeItem(file.name);
                  }

                } else {
                  // remove file info when upload percent is 100%
                  // avoid 499 bug
                  localStorage.removeItem(file.name);
                }
              } else {
                // remove file info when last upload time is over one day
                localStorage.removeItem(file.name);
              }
            }
            speedCalInfo.startTime = new Date().getTime();
            // TODO: to support bput
            // http://developer.qiniu.com/docs/v6/api/reference/up/bput.html
            up.setOption({
              'url': qiniuUploadUrl + '/mkblk/' + blockSize,
              'multipart': false,
              'chunk_size': chunk_size,
              'required_features': "chunks",
              'headers': {
                'Authorization': 'UpToken ' + that.token
              },
              'multipart_params': {}
            });
          }
        } else {
          logger.debug("directUpload because uploader.runtime !== 'html5' || uploader.runtime !== 'flash' || !chunk_size");
          // direct upload if runtime is not html5
          directUpload(up, file, that.key_handler);
        }
      });

      logger.debug("bind BeforeUpload event");

      // bind 'UploadProgress' event
      // calculate upload speed
      uploader.bind('UploadProgress', function(up, file) {
        logger.trace("UploadProgress event activated");
        speedCalInfo.currentTime = new Date().getTime();
        var timeUsed = speedCalInfo.currentTime - speedCalInfo.startTime; // ms
        var fileUploaded = file.loaded || 0;
        if (speedCalInfo.isResumeUpload) {
          fileUploaded = file.loaded - speedCalInfo.resumeFilesize;
        }
        file.speed = (fileUploaded / timeUsed * 1000).toFixed(0) || 0; // unit: byte/s
      });

      logger.debug("bind UploadProgress event");

      // bind 'ChunkUploaded' event
      // store the chunk upload info and set next chunk upload url
      uploader.bind('ChunkUploaded', function(up, file, info) {
        logger.debug("ChunkUploaded event activated");
        logger.debug("file: ", file);
        logger.debug("info: ", info);
        var res = that.parseJSON(info.response);
        logger.debug("res: ", res);
        // ctx should look like '[chunk01_ctx],[chunk02_ctx],[chunk03_ctx],...'
        ctx = ctx ? ctx + ',' + res.ctx : res.ctx;
        var leftSize = info.total - info.offset;
        var chunk_size = up.getOption && up.getOption('chunk_size');
        chunk_size = chunk_size || (up.settings && up.settings.chunk_size);
        if (leftSize < chunk_size) {
          up.setOption({
            'url': qiniuUploadUrl + '/mkblk/' + leftSize
          });
          logger.debug("up.setOption url: ", qiniuUploadUrl + '/mkblk/' + leftSize);
        }
        localStorage.setItem(file.name, that.stringifyJSON({
          ctx: ctx,
          percent: file.percent,
          total: info.total,
          offset: info.offset,
          time: (new Date()).getTime()
        }));
      });

      logger.debug("bind ChunkUploaded event");

      var retries = qiniuUploadUrls.length;

      // if error is unkown switch upload url and retry
      var unknow_error_retry = function(file){
        if (retries-- > 0) {
          setTimeout(function(){
            that.resetUploadUrl();
            file.status = plupload.QUEUED;
            uploader.stop();
            uploader.start();
          }, 0);
          return true;
        }else{
          retries = qiniuUploadUrls.length;
          return false;
        }
      };

      // bind 'Error' event
      // check the err.code and return the errTip
      uploader.bind('Error', (function(_Error_Handler) {
        return function(up, err) {
          logger.error("Error event activated");
          logger.error("err: ", err);
          var errTip = '';
          var file = err.file;
          if (file) {
            switch (err.code) {
              case plupload.FAILED:
                errTip = '上传失败。请稍后再试。';
                break;
              case plupload.FILE_SIZE_ERROR:
                var max_file_size = up.getOption && up.getOption('max_file_size');
                max_file_size = max_file_size || (up.settings && up.settings.max_file_size);
                errTip = '浏览器最大可上传' + max_file_size + '。更大文件请使用命令行工具。';
                break;
              case plupload.FILE_EXTENSION_ERROR:
                errTip = '文件验证失败。请稍后重试。';
                break;
              case plupload.HTTP_ERROR:
                if (err.response === '') {
                  // Fix parseJSON error ,when http error is like net::ERR_ADDRESS_UNREACHABLE
                  errTip = err.message || '未知网络错误。';
                  if (!unknow_error_retry(file)) {
                    return;
                  }
                  break;
                }
                var errorObj = that.parseJSON(err.response);
                var errorText = errorObj.error;
                switch (err.status) {
                  case 400:
                    errTip = "请求报文格式错误。";
                    break;
                  case 401:
                    errTip = "客户端认证授权失败。请重试或提交反馈。";
                    break;
                  case 405:
                    errTip = "客户端请求错误。请重试或提交反馈。";
                    break;
                  case 579:
                    errTip = "资源上传成功，但回调失败。";
                    break;
                  case 599:
                    errTip = "网络连接异常。请重试或提交反馈。";
                    if (!unknow_error_retry(file)) {
                      return;
                    }
                    break;
                  case 614:
                    errTip = "文件已存在。";
                    try {
                      errorObj = that.parseJSON(errorObj.error);
                      errorText = errorObj.error || 'file exists';
                    } catch (e) {
                      errorText = errorObj.error || 'file exists';
                    }
                    break;
                  case 631:
                    errTip = "指定空间不存在。";
                    break;
                  case 701:
                    errTip = "上传数据块校验出错。请重试或提交反馈。";
                    break;
                  default:
                    errTip = "未知错误。";
                    if (!unknow_error_retry(file)) {
                      return;
                    }
                    break;
                }
                errTip = errTip + '(' + err.status + '：' + errorText + ')';
                break;
              case plupload.SECURITY_ERROR:
                errTip = '安全配置错误。请联系网站管理员。';
                break;
              case plupload.GENERIC_ERROR:
                errTip = '上传失败。请稍后再试。';
                break;
              case plupload.IO_ERROR:
                errTip = '上传失败。请稍后再试。';
                break;
              case plupload.INIT_ERROR:
                errTip = '网站配置错误。请联系网站管理员。';
                uploader.destroy();
                break;
              default:
                errTip = err.message + err.details;
                if (!unknow_error_retry(file)) {
                  return;
                }
                break;
            }
            if (_Error_Handler) {
              _Error_Handler(up, err, errTip);
            }
          }
          up.refresh(); // Reposition Flash/Silverlight
        };
      })(_Error_Handler));

      logger.debug("bind Error event");

      // bind 'FileUploaded' event
      // intercept the complete of upload
      // - get downtoken from downtoken_url if bucket is private
      // - invoke mkfile api to compose chunks if upload strategy is chunk upload
      uploader.bind('FileUploaded', (function(_FileUploaded_Handler) {
        return function(up, file, info) {
          logger.debug("FileUploaded event activated");
          logger.debug("file: ", file);
          logger.debug("info: ", info);
          var last_step = function(up, file, info) {
            if (op.downtoken_url) {
              // if op.dowontoken_url is not empty
              // need get downtoken before invoke the _FileUploaded_Handler
              var ajax_downtoken = that.createAjax();
              ajax_downtoken.open('POST', op.downtoken_url, true);
              ajax_downtoken.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
              ajax_downtoken.onreadystatechange = function() {
                if (ajax_downtoken.readyState === 4) {
                  if (ajax_downtoken.status === 200) {
                    var res_downtoken;
                    try {
                      res_downtoken = that.parseJSON(ajax_downtoken.responseText);
                    } catch (e) {
                      throw ('invalid json format');
                    }
                    var info_extended = {};
                    plupload.extend(info_extended, that.parseJSON(info), res_downtoken);
                    if (_FileUploaded_Handler) {
                      _FileUploaded_Handler(up, file, that.stringifyJSON(info_extended));
                    }
                  } else {
                    uploader.trigger('Error', {
                      status: ajax_downtoken.status,
                      response: ajax_downtoken.responseText,
                      file: file,
                      code: plupload.HTTP_ERROR
                    });
                  }
                }
              };
              ajax_downtoken.send('key=' + that.parseJSON(info).key + '&domain=' + op.domain);
            } else if (_FileUploaded_Handler) {
              _FileUploaded_Handler(up, file, info);
            }
          };

          var res = that.parseJSON(info.response);
          ctx = ctx ? ctx : res.ctx;
          // if ctx is not empty
          //      that means the upload strategy is chunk upload
          //      befroe the invoke the last_step
          //      we need request the mkfile to compose all uploaded chunks
          // else
          //      invalke the last_step
          logger.debug("ctx: ", ctx);
          if (ctx) {
            var key = '';
            logger.debug("save_key: ", op.save_key);
            if (!op.save_key) {
              key = getFileKey(up, file, that.key_handler);
              key = key ? '/key/' + that.URLSafeBase64Encode(key) : '';
            }

            var fname = '/fname/' + that.URLSafeBase64Encode(file.name);

            logger.debug("op.x_vars: ", op.x_vars);
            var x_vars = op.x_vars,
              x_val = '',
              x_vars_url = '';
            if (x_vars !== undefined && typeof x_vars === 'object') {
              for (var x_key in x_vars) {
                if (x_vars.hasOwnProperty(x_key)) {
                  if (typeof x_vars[x_key] === 'function') {
                    x_val = that.URLSafeBase64Encode(x_vars[x_key](up, file));
                  } else if (typeof x_vars[x_key] !== 'object') {
                    x_val = that.URLSafeBase64Encode(x_vars[x_key]);
                  }
                  x_vars_url += '/x:' + x_key + '/' + x_val;
                }
              }
            }

            var url = qiniuUploadUrl + '/mkfile/' + file.size + key + fname + x_vars_url;

            var ie = that.detectIEVersion();
            var ajax;
            if (ie && ie <= 9) {
              ajax = new mOxie.XMLHttpRequest();
              mOxie.Env.swf_url = op.flash_swf_url;
            }else{
              ajax = that.createAjax();
            }
            ajax.open('POST', url, true);
            ajax.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8');
            ajax.setRequestHeader('Authorization', 'UpToken ' + that.token);
            var onreadystatechange = function(){
              logger.debug("ajax.readyState: ", ajax.readyState);
              if (ajax.readyState === 4) {
                localStorage.removeItem(file.name);
                var info;
                if (ajax.status === 200) {
                  info = ajax.responseText;
                  logger.debug("mkfile is success: ", info);
                  last_step(up, file, info);
                } else {
                  info = {
                    status: ajax.status,
                    response: ajax.responseText,
                    file: file,
                    code: -200
                  };
                  logger.debug("mkfile is error: ", info);
                  uploader.trigger('Error', info);
                }
              }
            };
            if (ie && ie <= 9) {
              ajax.bind('readystatechange', onreadystatechange);
            }else{
              ajax.onreadystatechange = onreadystatechange;
            }
            ajax.send(ctx);
            logger.debug("mkfile: ", url);
          } else {
            last_step(up, file, info.response);
          }

        };
      })(_FileUploaded_Handler));

      logger.debug("bind FileUploaded event");

      // init uploader
      uploader.init();

      logger.debug("invoke uploader.init()");

      logger.debug("init uploader end");

      return uploader;
    };

    /**
     * get url by key
     * @param  {String} key of file
     * @return {String} url of file
     */
    this.getUrl = function(key) {
      if (!key) {
        return false;
      }
      key = encodeURI(key);
      var domain = this.domain;
      if (domain.slice(domain.length - 1) !== '/') {
        domain = domain + '/';
      }
      return domain + key;
    };

    /**
     * invoke the imageView2 api of Qiniu
     * @param  {Object} api params
     * @param  {String} key of file
     * @return {String} url of processed image
     */
    this.imageView2 = function(op, key) {
      var mode = op.mode || '',
        w = op.w || '',
        h = op.h || '',
        q = op.q || '',
        format = op.format || '';
      if (!mode) {
        return false;
      }
      if (!w && !h) {
        return false;
      }

      var imageUrl = 'imageView2/' + mode;
      imageUrl += w ? '/w/' + w : '';
      imageUrl += h ? '/h/' + h : '';
      imageUrl += q ? '/q/' + q : '';
      imageUrl += format ? '/format/' + format : '';
      if (key) {
        imageUrl = this.getUrl(key) + '?' + imageUrl;
      }
      return imageUrl;
    };

    /**
     * invoke the imageMogr2 api of Qiniu
     * @param  {Object} api params
     * @param  {String} key of file
     * @return {String} url of processed image
     */
    this.imageMogr2 = function(op, key) {
      var auto_orient = op['auto-orient'] || '',
        thumbnail = op.thumbnail || '',
        strip = op.strip || '',
        gravity = op.gravity || '',
        crop = op.crop || '',
        quality = op.quality || '',
        rotate = op.rotate || '',
        format = op.format || '',
        blur = op.blur || '';
      //Todo check option

      var imageUrl = 'imageMogr2';

      imageUrl += auto_orient ? '/auto-orient' : '';
      imageUrl += thumbnail ? '/thumbnail/' + thumbnail : '';
      imageUrl += strip ? '/strip' : '';
      imageUrl += gravity ? '/gravity/' + gravity : '';
      imageUrl += quality ? '/quality/' + quality : '';
      imageUrl += crop ? '/crop/' + crop : '';
      imageUrl += rotate ? '/rotate/' + rotate : '';
      imageUrl += format ? '/format/' + format : '';
      imageUrl += blur ? '/blur/' + blur : '';

      if (key) {
        imageUrl = this.getUrl(key) + '?' + imageUrl;
      }
      return imageUrl;
    };

    /**
     * invoke the watermark api of Qiniu
     * @param  {Object} api params
     * @param  {String} key of file
     * @return {String} url of processed image
     */
    this.watermark = function(op, key) {
      var mode = op.mode;
      if (!mode) {
        return false;
      }

      var imageUrl = 'watermark/' + mode;

      if (mode === 1) {
        var image = op.image || '';
        if (!image) {
          return false;
        }
        imageUrl += image ? '/image/' + this.URLSafeBase64Encode(image) : '';
      } else if (mode === 2) {
        var text = op.text ? op.text : '',
          font = op.font ? op.font : '',
          fontsize = op.fontsize ? op.fontsize : '',
          fill = op.fill ? op.fill : '';
        if (!text) {
          return false;
        }
        imageUrl += text ? '/text/' + this.URLSafeBase64Encode(text) : '';
        imageUrl += font ? '/font/' + this.URLSafeBase64Encode(font) : '';
        imageUrl += fontsize ? '/fontsize/' + fontsize : '';
        imageUrl += fill ? '/fill/' + this.URLSafeBase64Encode(fill) : '';
      } else {
        // Todo mode3
        return false;
      }

      var dissolve = op.dissolve || '',
        gravity = op.gravity || '',
        dx = op.dx || '',
        dy = op.dy || '';

      imageUrl += dissolve ? '/dissolve/' + dissolve : '';
      imageUrl += gravity ? '/gravity/' + gravity : '';
      imageUrl += dx ? '/dx/' + dx : '';
      imageUrl += dy ? '/dy/' + dy : '';

      if (key) {
        imageUrl = this.getUrl(key) + '?' + imageUrl;
      }
      return imageUrl;
    };

    /**
     * invoke the imageInfo api of Qiniu
     * @param  {String} key of file
     * @return {Object} image info
     */
    this.imageInfo = function(key) {
      if (!key) {
        return false;
      }
      var url = this.getUrl(key) + '?imageInfo';
      var xhr = this.createAjax();
      var info;
      var that = this;
      xhr.open('GET', url, false);
      xhr.onreadystatechange = function() {
        if (xhr.readyState === 4 && xhr.status === 200) {
          info = that.parseJSON(xhr.responseText);
        }
      };
      xhr.send();
      return info;
    };

    /**
     * invoke the exif api of Qiniu
     * @param  {String} key of file
     * @return {Object} image exif
     */
    this.exif = function(key) {
      if (!key) {
        return false;
      }
      var url = this.getUrl(key) + '?exif';
      var xhr = this.createAjax();
      var info;
      var that = this;
      xhr.open('GET', url, false);
      xhr.onreadystatechange = function() {
        if (xhr.readyState === 4 && xhr.status === 200) {
          info = that.parseJSON(xhr.responseText);
        }
      };
      xhr.send();
      return info;
    };

    /**
     * invoke the exif or imageInfo api of Qiniu
     * according with type param
     * @param  {String} ['exif'|'imageInfo']type of info
     * @param  {String} key of file
     * @return {Object} image exif or info
     */
    this.get = function(type, key) {
      if (!key || !type) {
        return false;
      }
      if (type === 'exif') {
        return this.exif(key);
      } else if (type === 'imageInfo') {
        return this.imageInfo(key);
      }
      return false;
    };

    /**
     * invoke api of Qiniu like a pipeline
     * @param  {Array of Object} params of a series api call
     * each object in array is options of api which name is set as 'fop' property
     * each api's output will be next api's input
     * @param  {String} key of file
     * @return {String|Boolean} url of processed image
     */
    this.pipeline = function(arr, key) {
      var isArray = Object.prototype.toString.call(arr) === '[object Array]';
      var option, errOp, imageUrl = '';
      if (isArray) {
        for (var i = 0, len = arr.length; i < len; i++) {
          option = arr[i];
          if (!option.fop) {
            return false;
          }
          switch (option.fop) {
            case 'watermark':
              imageUrl += this.watermark(option) + '|';
              break;
            case 'imageView2':
              imageUrl += this.imageView2(option) + '|';
              break;
            case 'imageMogr2':
              imageUrl += this.imageMogr2(option) + '|';
              break;
            default:
              errOp = true;
              break;
          }
          if (errOp) {
            return false;
          }
        }
        if (key) {
          imageUrl = this.getUrl(key) + '?' + imageUrl;
          var length = imageUrl.length;
          if (imageUrl.slice(length - 1) === '|') {
            imageUrl = imageUrl.slice(0, length - 1);
          }
        }
        return imageUrl;
      }
      return false;
    };
  }

  var Qiniu = new QiniuJsSDK();

  global.Qiniu = Qiniu;

  global.QiniuJsSDK = QiniuJsSDK;

})( window );
